Презентация: https://disk.yandex.ru/i/9xDp32w7cQzDCw
Проведение данного исследования необходимо коллегам из отдела маркетинга, для определения причин оттока клиентов и составления стратегии по их удержанию. Для достижения целей и помощи коллегам нам предстоит провести исследовательский и статистический анализ данных, сегментировать клиентов и на основании этого сформулировать предложения, которые помогут сократить отток и лучше понимать потребности клиентов.
Датасет содержит данные о клиентах банка «Метанпром». Банк располагается в Ярославле и областных городах: Ростов Великий и Рыбинск.
Колонки:
userid — идентификатор пользователя,score — баллы кредитного скоринга,City — город,Gender — пол,Age — возраст,Objects — количество объектов в собственности,Balance — баланс на счёте,Products — количество продуктов, которыми пользуется клиент,CreditCard — есть ли кредитная карта,Loyalty — активный клиент,estimated_salary — заработная плата клиента,Churn — ушёл или нет.# здесь сразу импортируем все библиотеки, которые потребуются в работе
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
import seaborn as sns
from plotly import graph_objects as go
from scipy import stats as st
import warnings
warnings.filterwarnings('ignore')
import statistics as stat
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, silhouette_score
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.cluster import KMeans
from sklearn import tree
from sklearn.tree import DecisionTreeClassifier
#from IPython.display import Image
from sklearn.tree import plot_tree
import phik
from phik.report import plot_correlation_matrix
from phik import report
#import numba
#import click
# считаем данные и посмотрим первые строки DataFram-a
try:
df = pd.read_csv('bank_dataset.csv')
except:
df = pd.read_csv('/datasets/bank_dataset.csv')
df.head()
# параметры формата
pd.set_option('display.float_format', '{:.2f}'.format)
df.info()
print(1 - df['Balance'].count()/df.shape[0])
# проверка на наличие явных дубликатов
df.duplicated().sum()
Имеем таблицу, содержащую 10 тысяч строк.
CreditCard, Loyalty, Churn больше подходит тип данных boolean;userid не несет для нашего исследования важной информации и будет мешать;gender и city с категориальными строковыми значениями добавим числовые кодирующие столбцы, для удобства дальнейшего анализа;Balance около 36% от общего числа записей.Рассмотрим проблемный столбец Balance более подробно
# посмотрим детальнее на столбец Balance
df['Balance'].describe().to_frame()
Разброс по балансу составляет от 3 768 до 250 898. При этом нет нулевых балансов.
Принимая во внимание предметную область, можно предположить, две причины возникновения пропусков
В первом случае нам нечем заполнить пропуски, медианные и средние значения для клиентов с аналогичными социально-демографическими чертами не будут релевантны.
С другой стороны сложно представить, что в выборке из 10 тысяч клиентов нет людей с нулевым балансом, т.к. причин его возникновения может быть масса, например:
Второе предположение кажется мне более правдоподобным, хотя и вероятность сбоя исключать нельзя. Поскольку избавиться от строк с пропусками мы не можем, ввиду их большого числа, поэтому попробуем заменить пропущенные значения нулями.
Вывод
На данном шаге познакомились с представленными данными. Сформулируем финальный список задач по предобработке.
CreditCard, Loyalty, Churn - привести к типу boolean;userid;Balance нулями.Переходим к следующему шагу.
Проводим предобработку по списку определенному на шаге 2.
# приведоим строки к нижнему регистру
df.columns = df.columns.str.lower()
# убираем столбец userid
df = df.drop('userid', axis=1)
# заменяем пропуски нулями
df = df.fillna(0)
Построим матрицу корреляций phic с балансом и категориальными переменными.
cols_to_keep = ["balance", "city", "gender", "churn", "products", "loyalty", "creditcard"]
df_phic = df[cols_to_keep]
phik_overview = df_phic.phik_matrix()
phik_overview.round(2).head(1)
Баланс сильнее всего коррелирует с городом и количеством продуктов. Далее посмотрим какова доля пропущеных значений баланса в каждой из категорий (город, продукты)
display (df.query("balance == 0").groupby('city')['balance'].count()/df.groupby('city')['age'].count())
display (df.query("balance == 0").groupby('products')['balance'].count()/df.groupby('products')['age'].count())
Пропуски баланса харакерны только для одного города Ростов Великий. Интересно, что доля пропусков в Рыбинске и Ярославле одинаковая, чуть менее половины. Таким образом пропуски можно отнести к категории MNAR (Missing Not At Random / Отсутствует не случайно) — пропуски зависят от данных, их нельзя отбрасывать, т.к. это приведёт к заметным искажениям.
# добавляем кодирующие столбцы для города
df['yaroslavl'] = 0
df.loc[df['city'] == 'Ярославль', 'yaroslavl'] = 1
df['rybinsk'] = 0
df.loc[df['city'] == 'Рыбинск', 'rybinsk'] = 1
df['rostov'] = 0
df.loc[df['city'] == 'Ростов Великий', 'rostov'] = 1
# добавляем кодирующие столбцы для гендерного признака
df.loc[df['gender'] == 'М', 'gender_bool'] = 0
df.loc[df['gender'] == 'Ж', 'gender_bool'] = 1
pd.get_dummies(df['city']).join(
df['gender'].map({'М':0,'Ж':1})
)
df.head()
На предыдущем шаге мы предположили, что нулевой баланс может быть у пользователей закрывающихся кредитных карт. Проверим у скольких пользователей КК среди строк в которых мы установили баланс равным нулю.
df.query("creditcard == 1 & balance==0").shape[0]
Из 3617 пропусков в балансе 2592 пришлось на владельцев кредитных карт, что косвенно подтверждает предположение о закрывающейся кредитке, но и не отменяет варианта с техническим сбоем.
Вывод
На данном шаге провели предобработку данных согласно списку, определенному в шаге 2.
Для начала посмотрим значения ключевых метрик для имеющихся данных.
df.describe().T
Можно отметить следующее:
Чтобы лучше понимать это значение посмотрим градацию коллег из совкомбанка
https://sovcombank.ru/blog/krediti/chto-takoe-kreditnii-reiting
0 — 300 баллов: очень низкий, получить кредит практически невозможно.
300 — 500 баллов: низкий, получить кредит очень сложно.
500 — 600 баллов: средний, получить кредит будет непросто.
600 — 700 баллов: хороший, получить кредит будет довольно просто.
700 — 850 баллов: очень хороший, банки выстраиваются к вам в очередь со своими предложениями.
Посмотрим на графики распределения. Вначале для категориальных величин.
sns.set_style("darkgrid")
column_list = ['city', 'gender', 'creditcard', 'loyalty', 'products', 'churn']
fig, ax = plt.subplots(2, 3)
fig.set_size_inches(15, 6)
fig.set_dpi(300)
for variable, subplot in zip(column_list, ax.flatten()):
splot = sns.countplot(df[variable], ax=subplot)
for p in splot.patches:
splot.annotate('{:.0f}'.format(p.get_height()), (p.get_x() + p.get_width()/2., p.get_height()), ha = 'center', va = 'center', xytext = (0, 10), textcoords = 'offset points')
fig.suptitle("Количество клиентов в разрезе категориальных переменных");
на графиках видим:
Теперь построим распределения для количественных данных
sns.set_style("darkgrid")
column_list = ['score', 'age', 'balance', 'estimated_salary']
fig, axs = plt.subplots(len(column_list) // 2, 2)
fig.set_size_inches(10, 8)
fig.set_dpi(300)
new_axs = [item for sublist in axs for item in sublist]
for i, column in enumerate(column_list):
sns.histplot(data=df, x=column, stat='density', common_norm=False, palette="Blues_d", ax=new_axs[i])
new_axs[i].set_title('Распределения признака {}'.format(column))
plt.tight_layout()
plt.show();
params = dict(
data=df,
x='age',
stat='density',
binrange=(15,95)
)
sns.histplot(**params, binwidth=5, lw=0);
sns.histplot(**params, binwidth=2, fc=(0,0,0,0));
Далее построим корреляционную таблицу
plt.figure(figsize=(15, 15))
sns.heatmap(df.corr(), annot = True, fmt='.0%', vmin=-1, vmax=1, center= 0, cmap= 'coolwarm', linewidths=1, linecolor='black')
plt.title('Тепловая карта по признакам')
plt.xticks(fontsize=14, rotation=90)
plt.yticks(fontsize=14, rotation=360)
plt.ylabel('Признаки')
plt.xlabel('Признаки');
df_corr = df.corr().drop('churn').sort_values('churn', ascending=False)
plt.figure(figsize=(3, 9))
sns.heatmap(df_corr[['churn']], annot = True, fmt='.0%', vmin=-1, vmax=1, center= 0, cmap= 'coolwarm', linewidths=1, linecolor='black')
plt.title('Тепловая карта по признакам')
plt.xticks(fontsize=14, rotation=90)
plt.yticks(fontsize=14, rotation=360)
plt.ylabel('Признаки')
plt.xlabel('Признаки');
df_phik = df.phik_matrix().drop('churn').sort_values('churn', ascending=False)
plt.figure(figsize=(3, 9))
sns.heatmap(df_phik[['churn']], annot = True, fmt='.0%', vmin=0, vmax=1, center= 0, cmap= 'coolwarm', linewidths=1, linecolor='black')
plt.title('Тепловая карта по признакам')
plt.xticks(fontsize=14, rotation=90)
plt.yticks(fontsize=14, rotation=360)
plt.ylabel('Признаки')
plt.xlabel('Признаки');
На корреляионной таблице каких - то сильных линейных зависимостей не наблюдаем, можно отметить следующие небольшие корреляции:
На phik корреляцинной таблице прослеживается связь оттока с количеством продуктов и возрастом в чуть меньшей степени с городом Ростов, активностью балансом и полом
df.groupby('products').mean().T
df.groupby('products')['age'].count()
Наблюдаем просто катастрофическую долю оттока у клиентов, пользующихся 3-4 продуктами. Доля оттока у них составляет 82-100% Несмотря на их небольшое количество следует отдельно обратить внимание на эти группы.
Также довольно высокую долю оттока наблюдаем у клиентов с одним продуктом - порядка 28%.
Построим распределения в разрезе количества потребляемых продуктов. Посколько группы 3 и 4 малочисленны, объединим их в одну и обозначим как 3.
df.loc[df['products'] == 4, 'products'] = 3
values = ['city', 'gender', 'creditcard', 'loyalty', 'churn', 'objects']
fig, ax = plt.subplots(2, 3)
fig.set_size_inches(15, 7)
fig.set_dpi(300)
for variable, subplot in zip(values, ax.flatten()):
splot = sns.countplot(data=df, x=variable, hue='products', ax=subplot)
for p in splot.patches:
splot.annotate('{:.0f}'.format(p.get_height()), (p.get_x() + p.get_width()/2., p.get_height()), ha = 'center', va = 'center', xytext = (0, 10), textcoords = 'offset points')
fig.suptitle("Количество клиентов в разрезе категориальных переменных и продуктов");
Ключевые метрика для нас — это отток, поэтому анализируя графики с учетом того, что лучшая группа по оттоку — это 2, худшая 3, в группе с 1 продуктом значение оттока также оставляет желать лучшего.
columns_list = ['score', 'age', 'balance', 'estimated_salary']
fig, axs = plt.subplots(len(columns_list) // 2, 2)
fig.set_size_inches(10, 8)
fig.set_dpi(300)
new_axs = [item for sublist in axs for item in sublist]
for i, column in enumerate(columns_list):
sns.histplot(data=df, x=column, stat='density', hue = 'products', common_norm=False, ax=new_axs[i])
new_axs[i].set_title('Распределения признака {}'.format(column))
plt.tight_layout()
plt.show();
columns_list = ['score', 'age', 'balance', 'estimated_salary']
fig, axs = plt.subplots(len(columns_list) // 2, 2)
fig.set_size_inches(10, 8)
fig.set_dpi(300)
for i, column in enumerate(columns_list):
sns.histplot(
data=df.assign(products=df.products.clip(1,3)),
x=column,
stat='density',
hue = 'products',
multiple='dodge',
palette='tab10',
kde=True,
common_norm=False,
bins=14,
ax=axs.ravel()[i])\
.set(title = f'Распределение признака {column}', ylabel="Плотность вероятности")
plt.tight_layout()
plt.show();
Теперь посмотрим на средние значения в разрезе отттока.
df.groupby('churn').mean().T
На первый взгляд можно отметить следующее:
Далее посмотрим на распределения по категориальным переменным в разрезе оттока
values = ['city', 'gender', 'creditcard', 'loyalty', 'products', 'objects']
fig, ax = plt.subplots(2, 3)
fig.set_size_inches(15, 9)
fig.set_dpi(300)
for variable, subplot in zip(values, ax.flatten()):
splot = sns.countplot(data=df, x=variable, hue='churn', ax=subplot)
for p in splot.patches:
splot.annotate('{:.1f}%'.format(p.get_height()/len(df[variable])*100), (p.get_x() + p.get_width()/2., p.get_height()), ha = 'center', va = 'center', xytext = (0, 10), textcoords = 'offset points')
fig.suptitle("Количество клиентов в разрезе категориальных переменных и оттока");
Изучив полученные графики можем заключить следующее:
object мало информативен, т.к. мы не знаем, что за ним стоит "старый ржавый мотоцикл" или "дворец в Геленджике"columns_list = ['score', 'age', 'balance', 'estimated_salary']
fig, axs = plt.subplots(len(columns_list) // 2, 2)
fig.set_size_inches(10, 8)
fig.set_dpi(300)
new_axs = [item for sublist in axs for item in sublist]
for i, column in enumerate(columns_list):
sns.histplot(data=df, x=column, stat='density', hue = 'churn', common_norm=False, ax=new_axs[i])
new_axs[i].set_title('Распределения признака {}'.format(column))
plt.tight_layout()
plt.show();
Рассмотрев графики распределения количественных признаков, можем дополнить портрет отточного клиента следующими штрихами:
Вывод
Резюмируем все вышесказанное и сформулируем портреты отточного и не отточного клиентов.
Чаще всего в отток попадают:
Менее склонны к оттоку:
Сформулируем первую гипотезу.
α=5% критический уровень статистической значимости.
Разделим данные по оттоку и проверим значения дисперсий в получившихся совокупностях.
print (stat.variance(df['estimated_salary']))
print (stat.variance(df.query('churn == 1')['estimated_salary']))
print (stat.variance(df.query('churn == 0')['estimated_salary']))
Далее определимся какой статистический тест подойдет лучше для проверки первой гипотезы.
воспользуемся ttest, equal_var=False т.к. дисперсии сопоставимы, но не равны.
df_churn = df.query('churn == 1')['estimated_salary']
df_not_churn = df.query('churn == 0')['estimated_salary']
alpha = 0.05
results = st.ttest_ind(
df_churn,
df_not_churn,
equal_var=True)
print('p-значение:', results.pvalue)
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу")
else:
print("Не получилось отвергнуть нулевую гипотезу")
Нет основания отвергать нулевую гипотезу, считаем, что средний доход отточных и неотточных клиентов равен
Сформулируем вторую гипотезу
α=5% критический уровень статистической значимости.
# проверим значения дисперсий
print (stat.variance(df.query('churn == 1')['age']))
print (stat.variance(df.query('churn == 0')['age']))
Далее определимся какой статистический тест подойдет лучше для проверки первой гипотезы.
воспользуемся ttest, equal_var=False т.к. дисперсии сопоставимы, но не равны.
df_churn = df.query('churn == 1')['age']
df_not_churn = df.query('churn == 0')['age']
alpha = 0.05
results = st.ttest_ind(
df_churn,
df_not_churn,
equal_var=False)
print('p-значение:', results.pvalue)
if (results.pvalue < alpha):
print("Отвергаем нулевую гипотезу")
else:
print("Не получилось отвергнуть нулевую гипотезу")
Есть основания отвергнуть нулевую гипотезу, считаем, что средний возраст отточных и неотточных клиентов различается
Вывод
на данном шаге проверены две гипотезы:
по итогу можем сказать:
Имеем задачу бинарной классификации. Сильно скоррелированных признаков нет.
Выделение обучающей и валидационной выборок
# убираем строковые категориальные данные
df_for_model = df.drop(['city', 'gender'], axis=1)
# разделим наши данные на признаки (матрица X) и целевую переменную (y)
X = df_for_model.drop('churn', axis=1)
y = df_for_model['churn']
# разделяем модель на обучающую и валидационную выборку
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=0)
Стандартизация данных
# создадим объект класса StandardScaler и применим его к обучающей выборке
scaler = StandardScaler()
#обучаем scaler и одновременно трансформируем матрицу для обучающей выборки
X_train_st = scaler.fit_transform(X_train)
#применяем стандартизацию к матрице признаков для тестовой выборки
X_test_st = scaler.transform(X_test)
Обучение модели на train-выборке: логистической регрессией, случайным лесом, дерево принятия решений
# задаем алгоритм для нашей модели, сначала Логистическая регрессия
model_LR = LogisticRegression(solver='liblinear', random_state=0)
# обучение модели
model_LR.fit(X_train_st, y_train)
# воспользуйтесь уже обученной моделью, чтобы сделать прогнозы
predictions_LR = model_LR.predict(X_test_st)
probabilities_LR = model_LR.predict_proba(X_test_st)[:,1]
# случайный лес
model_RF = RandomForestClassifier(random_state=0)
model_RF.fit(X_train_st, y_train)
predictions_RF = model_RF.predict(X_test_st)
probabilities_RF = model_RF.predict_proba(X_test_st)[:,1]
# дерево принятия решений
tree_model = DecisionTreeClassifier(min_samples_leaf=500)
tree_model.fit(X_train, y_train)
predictions_TM = tree_model.predict(X_test)
Посмотрим на значения метрик для определения наилучшей модели
print ('Значения метрик для модели логистической регресси')
print ('accuracy = ', round(accuracy_score(y_test, predictions_LR), 2))
print ('precision = ', round(precision_score(y_test, predictions_LR), 2))
print ('recall = ', round(recall_score(y_test, predictions_LR), 2))
print ('F1 = ', round(f1_score(y_test, predictions_LR), 2))
print ('Значения метрик для модели случайный лес')
print ('accuracy = ', round(accuracy_score(y_test, predictions_RF), 2))
print ('precision = ', round(precision_score(y_test, predictions_RF), 2))
print ('recall = ', round(recall_score(y_test, predictions_RF), 2))
print ('F1 = ', round(f1_score(y_test, predictions_RF), 2))
print ('Значения метрик для модели дерево решений')
print ('accuracy = ', round(accuracy_score(y_test, predictions_TM), 2))
print ('precision = ', round(precision_score(y_test, predictions_TM), 2))
print ('recall = ', round(recall_score(y_test, predictions_TM), 2))
print ('F1 = ', round(f1_score(y_test, predictions_TM), 2))
Основной метрикой будем считать F1. т.к. метрика полноты accuracy хорошо работает только при условии баланса классов — когда объектов каждого класса примерно поровну, 50% : 50%. Метрики precision и recall направлены на избежание противоположных рисков, нужна сводная метрика, учитывающая баланс между метриками. Это F1-score.
Таким образом лучшей моделью в нашем случае считаем Случайный лес со значениями F1 = 0,62, неплохим значением Точности (precision) = 0.75, и полнотой (recall) хотя бы чуть больше чем 0.5.
Случайный лес - это сводный алгоритм состоит из множества деревьев. Визуализируем одно из возможных деревьев на полученной ранее модели дерева принятия решений.
plt.figure(figsize = (20,15)) # задайте размер фигуры, чтобы получить крупное изображение
plot_tree(tree_model, filled=True, feature_names = X_train.columns, class_names = ['not churn', 'churn'])
plt.show()
tree_model_2 = DecisionTreeClassifier(class_weight = "balanced",max_depth=2)
tree_model_2.fit(X_train, y_train)
predictions = tree_model_2.predict(X_test)
print ('F1 = ', round(f1_score(y_test, predictions), 2))
plt.figure(figsize = (20,15))
plot_tree(tree_model_2, filled=True, feature_names = X_train.columns, class_names = ['not churn', 'churn'])
plt.show()
### КОД РЕВЬЮЕРА
pd.DataFrame((model_RF.feature_importances_, model_LR.coef_[0]),
index=['RF','LR'],
columns = X.columns).T
Вывод
На данном шаге построили несколько моделей прогнозирования оттока клиентов, по совокупности метрик лучшей считаем Случайный лес
На данном шаге проведем кластеризацию клиентов с помощью KMeans и посмотрим, что нам это даст.
X = df_for_model.drop(['churn'], axis=1)
# стандартизируем данные
sc = StandardScaler()
X_sc = sc.fit_transform(X)
# в переменной linked сохранена таблица «связок» между объектами, её можно визуализировать как дендрограмму
linked = linkage(X_sc, method = 'ward')
#X.head()
# строим дендрограмму
plt.figure(figsize=(15, 10))
dendrogram(linked, orientation='top')
plt.title('Hierarchial clustering')
plt.show()
Количество кластеров определим равное трем.
km = KMeans(n_clusters=3, random_state=0) # задаём число кластеров, равное 3, и фиксируем значение random_state для воспроизводимости результата
labels = km.fit_predict(X_sc)
# добавляем в исходные данные метки с номерами кластеров
df['cluster_km'] = labels
#df.head()
Посмотрим средние значения параметров по кластерам.
round (df.groupby('cluster_km').mean().T, 2)
Количество записей в каждом кластере.
df.groupby('cluster_km')['gender'].count()
Оценим качетство проведенной кластеризации
print('Silhouette_score: = ', silhouette_score(X_sc, labels))
Получаем Silhouette_score на уровне около 0.2. Могло бы быть и лучше.
Вывод
Ожидаемо в кластер с самым высоким оттоком попали клиенты
все это не противоречит ранее сделанным наблюдениям, что уже хорошо, но и какой - то существенно новой информации не дает.
Итак, к стратегически важным можем отнесте следующие параметры: gender, age, city, balance, products
Посмотрим доли килентов с потенциально опасным значением данных признаков от общего числа клиентов.
print ('Женщины', df.query("gender == 'Ж'").shape[0]/df.shape[0]*100, '%')
print ('Возраст больше 42', round(df.query("age>42").shape[0]/df.shape[0]*100,2), '%')
print ('Ростов Великий', round(df.query("city == 'Ростов Великий'").shape[0]/df.shape[0]*100,2), '%')
print ('Высокий баланс', round(df.query("balance>100000").shape[0]/df.shape[0]*100,2), '%')
print ('Пользователи 1, 3 или 4 продуктов', round(df.query("products != 2").shape[0]/df.shape[0]*100,2), '%')
Да имеем огромные доли вплоть до половины датасета. Посчитаем долю оттока в каждой из этих групп.
df_gender = df.query("gender == 'Ж'")
df_age = df.query("age>42")
df_city = df.query("city == 'Ростов Великий'")
df_balance = df.query("balance>100000")
df_products = df.query("products != 2")
part_churn = df.groupby('churn')['churn'].count() / df.shape[0]
display (part_churn)
part_churn.plot(kind = 'pie').set(title = 'общий отток')
values = [df_gender, df_age, df_city, df_balance, df_products]
title_list = ['женщины', 'старше 42', 'из Ростова', 'высокий баланс', 'не 2 продукта']
fig, axs = plt.subplots(1, len(values), sharey = False, figsize = (15,5))
for i, value in enumerate(values):
part_churn = value.groupby('churn')['churn'].count() / value.query("churn==1")['age'].count()
part_churn.plot(kind = 'pie', ax=axs[i]).set(title = title_list[i])
df_segment = pd.DataFrame()
df_segment['name'] = title_list
df_segment['size'] = 0
df_segment['avg_churn'] = 0.0
for i, value in enumerate(values):
df_segment['size'][i] = value.shape[0]
df_segment['avg_churn'][i] = value.query("churn==1").shape[0] / value.shape[0]
df_segment
Во всех группах довольно высокий отток, выше общего по данным. Схожи по доле оттока сегменты: Женщины и высокий баланс, Ростов Великий и не 2 продукта. Объединим их в один сегмент.
df_gender_balance = df.query("gender == 'Ж' & balance>100000")
df_city_products = df.query("city == 'Ростов Великий' & products!= 2")
values = [df_gender_balance, df_city_products, df_age]
title_list = ['женщины c высоким балансом', 'Ростов не 2 продукта', 'старше 42']
fig, axs = plt.subplots(1, len(values), sharey = False, figsize = (15,5))
for i, value in enumerate(values):
part_churn = value.groupby('churn')['churn'].count() / value.query("churn==1")['churn'].count()
part_churn.plot(kind = 'pie', ax=axs[i]).set(title = title_list[i])
values = [df_gender_balance, df_city_products, df_age]
df_segment = pd.DataFrame()
df_segment['name'] = title_list
df_segment['size'] = 0
df_segment['avg_churn'] = 0.0
for i, value in enumerate(values):
df_segment['size'][i] = value.shape[0]
df_segment['avg_churn'][i] = value.query("churn==1").shape[0] / value.shape[0]
df_segment.sort_values('avg_churn', ascending = False).reset_index()
Имеем три сегмента с крайне высоким оттоком
Размеры сегментов от 1496 до 2148 человек с долей оттока от 30 до 47% т.е. достоточно крупные и высокоотчные.
Вывод
Рекомендации по работе с сегментами можно дать следующие:
В данной работе были проанализированы данные о клиентах банка "Метанпром".
Построены модели прогнозирования оттока клиентов:
* логистическая регрессия;
* случайный лес;
* дерево принятия решений.
Основываясь на показаниях ключевых метрик оценки моделей accurancy, precision, recall, F1 можно сказать, что обе модели хорошо показывает себя в прогнозировании оттока. По совокупности чуть лучше показывает себя модель случайный лес.
Выделены основные черты клиентов склонных к оттоку
и более надежных:
Проверены две статистические гипотезы
Можно утверждать, что:
Проведена кластеризация
Использовался метод KMeans, выделено три кластера.
Качество проведенной кластеризации не высокое Silhouette_score= 0,2, однако средние значения характеристик по кластерам не противоречат и подтверждают сделанные ранее выводы о портретах клиентов.
Общие рекомендации отделу маркетинга:
1 - необходимы активности направленные на отточных клиентов, в частности:
Здесь требуется проработка партнерских программ и экономической целесообразности.
2 - уделить внимание контролю качества, удовлетворенности от использования продуктов у потенциально отточных клиентов
Обратная связь - это очень важно в любом бизнесе, а особенно в банковском/
3 - увеличивать число клиентов, у которых будут соблюдаться маркеры характеризующие слабый отток:
Рекомендации отделу маркетинга по работе с наиболее проблемными сегментами
Первоочередное воздействие необходимо на группу старше 42 лет либо "Ростов не 2 продукта"
*предварительно убедившись, что их устраивает качество обслуживания и отработав возможные возражения / негатив;
Реализация всех вышеописанных рекомендаций требует тщательной проработки профильными подразделениями банка